// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

import QtQuick 2.15
import QtQuick.Templates 2.15 as T
import StudioTheme 1.0 as StudioTheme

T.SpinBox {
    id: mySpinBox

    property real realFrom: 0.0
    property real realTo: 99.0
    property real realValue: 1.0
    property real realStepSize: 1.0

    property alias labelColor: spinBoxInput.color
    property alias actionIndicator: actionIndicator

    property int decimals: 0

    property real minStepSize: {
        var tmpMinStepSize = Number((mySpinBox.realStepSize * 0.1).toFixed(mySpinBox.decimals))
        return (tmpMinStepSize) ? tmpMinStepSize : mySpinBox.realStepSize
    }
    property real maxStepSize: {
        var tmpMaxStepSize = Number((mySpinBox.realStepSize * 10.0).toFixed(mySpinBox.decimals))
        return (tmpMaxStepSize < mySpinBox.realTo) ? tmpMaxStepSize : mySpinBox.realStepSize
    }

    property bool edit: spinBoxInput.activeFocus
    // This property is used to indicate the global hover state
    property bool hover: (spinBoxInput.hover || actionIndicator.hover || spinBoxIndicatorUp.hover
                         || spinBoxIndicatorDown.hover || sliderIndicator.hover)
                         && mySpinBox.enabled
    property bool drag: false
    property bool sliderDrag: sliderPopup.drag

    property bool dirty: false // user modification flag

    // TODO Not used anymore. Will be removed when all dependencies were removed.
    property real realDragRange: mySpinBox.realTo - mySpinBox.realFrom

    property alias actionIndicatorVisible: actionIndicator.visible
    property real __actionIndicatorWidth: StudioTheme.Values.actionIndicatorWidth
    property real __actionIndicatorHeight: StudioTheme.Values.actionIndicatorHeight

    property bool spinBoxIndicatorVisible: true
    property real __spinBoxIndicatorWidth: StudioTheme.Values.spinBoxIndicatorWidth
    property real __spinBoxIndicatorHeight: StudioTheme.Values.spinBoxIndicatorHeight

    property alias sliderIndicatorVisible: sliderIndicator.visible
    property real __sliderIndicatorWidth: StudioTheme.Values.sliderIndicatorWidth
    property real __sliderIndicatorHeight: StudioTheme.Values.sliderIndicatorHeight

    property alias __devicePixelRatio: spinBoxInput.devicePixelRatio
    property alias pixelsPerUnit: spinBoxInput.pixelsPerUnit

    property alias compressedValueTimer: myTimer

    property string preFocusText: ""

    signal realValueModified
    signal compressedRealValueModified
    signal dragStarted
    signal dragEnded
    signal dragging
    signal indicatorPressed

    // Use custom wheel handling due to bugs
    property bool __wheelEnabled: false
    wheelEnabled: false
    hoverEnabled: true

    width: StudioTheme.Values.defaultControlWidth
    height: StudioTheme.Values.defaultControlHeight

    leftPadding: spinBoxIndicatorDown.x + spinBoxIndicatorDown.width
    rightPadding: sliderIndicator.width + StudioTheme.Values.border

    font.pixelSize: StudioTheme.Values.myFontSize
    editable: true

    // Leave this in for now
    from: -99
    value: 0
    to: 99

    validator: DoubleValidator {
        id: doubleValidator
        locale: mySpinBox.locale.name
        notation: DoubleValidator.StandardNotation
        decimals: mySpinBox.decimals
        bottom: Math.min(mySpinBox.realFrom, mySpinBox.realTo)
        top: Math.max(mySpinBox.realFrom, mySpinBox.realTo)
    }

    ActionIndicator {
        id: actionIndicator
        myControl: mySpinBox
        x: 0
        y: 0
        width: actionIndicator.visible ? mySpinBox.__actionIndicatorWidth : 0
        height: actionIndicator.visible ? mySpinBox.__actionIndicatorHeight : 0
    }

    up.indicator: RealSpinBoxIndicator {
        id: spinBoxIndicatorUp
        myControl: mySpinBox
        iconFlip: -1
        visible: mySpinBox.spinBoxIndicatorVisible
        onRealPressed: mySpinBox.indicatorPressed()
        onRealReleased: mySpinBox.realIncrease()
        onRealPressAndHold: mySpinBox.realIncrease()
        x: actionIndicator.width + StudioTheme.Values.border
        y: StudioTheme.Values.border
        width: mySpinBox.spinBoxIndicatorVisible ? mySpinBox.__spinBoxIndicatorWidth : 0
        height: mySpinBox.spinBoxIndicatorVisible ? mySpinBox.__spinBoxIndicatorHeight : 0

        realEnabled: (mySpinBox.realFrom < mySpinBox.realTo) ? (mySpinBox.realValue < mySpinBox.realTo)
                                                             : (mySpinBox.realValue > mySpinBox.realTo)
    }

    down.indicator: RealSpinBoxIndicator {
        id: spinBoxIndicatorDown
        myControl: mySpinBox
        visible: mySpinBox.spinBoxIndicatorVisible
        onRealPressed: mySpinBox.indicatorPressed()
        onRealReleased: mySpinBox.realDecrease()
        onRealPressAndHold: mySpinBox.realDecrease()
        x: actionIndicator.width + StudioTheme.Values.border
        y: spinBoxIndicatorUp.y + spinBoxIndicatorUp.height
        width: mySpinBox.spinBoxIndicatorVisible ? mySpinBox.__spinBoxIndicatorWidth : 0
        height: mySpinBox.spinBoxIndicatorVisible ? mySpinBox.__spinBoxIndicatorHeight : 0

        realEnabled: (mySpinBox.realFrom < mySpinBox.realTo) ? (mySpinBox.realValue > mySpinBox.realFrom)
                                                             : (mySpinBox.realValue < mySpinBox.realFrom)
    }

    contentItem: RealSpinBoxInput {
        id: spinBoxInput
        myControl: mySpinBox
        validator: doubleValidator

        function handleEditingFinished() {
            mySpinBox.focus = false

            // Keep the dirty state before calling setValueFromInput(),
            // it will be set to false (cleared) internally
            var valueModified = mySpinBox.dirty

            mySpinBox.setValueFromInput()
            myTimer.stop()

            // Only trigger the signal, if the value was modified
            if (valueModified)
                mySpinBox.compressedRealValueModified()
        }

        onEditingFinished: spinBoxInput.handleEditingFinished()
        onTextEdited: mySpinBox.dirty = true
    }

    background: Rectangle {
        id: spinBoxBackground
        color: StudioTheme.Values.themeControlOutline
        border.color: StudioTheme.Values.themeControlOutline
        border.width: StudioTheme.Values.border
        x: actionIndicator.width
        width: mySpinBox.width - actionIndicator.width
        height: mySpinBox.height
    }

    CheckIndicator {
        id: sliderIndicator
        myControl: mySpinBox
        myPopup: sliderPopup
        x: spinBoxInput.x + spinBoxInput.width
        y: StudioTheme.Values.border
        width: sliderIndicator.visible ? mySpinBox.__sliderIndicatorWidth - StudioTheme.Values.border : 0
        height: sliderIndicator.visible ? mySpinBox.__sliderIndicatorHeight - (StudioTheme.Values.border * 2) : 0
        visible: false // reasonable default
    }

    RealSliderPopup {
        id: sliderPopup
        myControl: mySpinBox
        x: actionIndicator.width + StudioTheme.Values.border
        y: StudioTheme.Values.height
        width: mySpinBox.width - actionIndicator.width - (StudioTheme.Values.border * 2)
        height: StudioTheme.Values.sliderHeight

        enter: Transition {}
        exit: Transition {}
    }

    textFromValue: function (value, locale) {
        locale.numberOptions = Locale.OmitGroupSeparator
        return Number(mySpinBox.realValue).toLocaleString(locale, 'f', mySpinBox.decimals)
    }

    valueFromText: function (text, locale) {
        mySpinBox.setRealValue(Number.fromLocaleString(locale, spinBoxInput.text))
        return 0
    }

    states: [
        State {
            name: "default"
            when: mySpinBox.enabled && !mySpinBox.hover && !mySpinBox.hovered
                  && !mySpinBox.edit && !mySpinBox.drag && !mySpinBox.sliderDrag
            PropertyChanges {
                target: mySpinBox
                __wheelEnabled: false
            }
            PropertyChanges {
                target: spinBoxInput
                selectByMouse: false
            }
            PropertyChanges {
                target: spinBoxBackground
                color: StudioTheme.Values.themeControlBackground
                border.color: StudioTheme.Values.themeControlOutline
            }
        },
        State {
            name: "edit"
            when: mySpinBox.edit
            PropertyChanges {
                target: mySpinBox
                __wheelEnabled: true
            }
            PropertyChanges {
                target: spinBoxInput
                selectByMouse: true
            }
            PropertyChanges {
                target: spinBoxBackground
                color: StudioTheme.Values.themeControlBackgroundInteraction
                border.color: StudioTheme.Values.themeControlOutline
            }
        },
        State {
            name: "drag"
            when: mySpinBox.drag || mySpinBox.sliderDrag
            PropertyChanges {
                target: spinBoxBackground
                color: StudioTheme.Values.themeControlBackgroundInteraction
                border.color: StudioTheme.Values.themeControlOutlineInteraction
            }
        },
        State {
            name: "disable"
            when: !mySpinBox.enabled
            PropertyChanges {
                target: spinBoxBackground
                color: StudioTheme.Values.themeControlOutlineDisabled
                border.color: StudioTheme.Values.themeControlOutlineDisabled
            }
        }
    ]

    Timer {
        id: myTimer
        repeat: false
        running: false
        interval: 400
        onTriggered: mySpinBox.compressedRealValueModified()
    }

    onRealValueChanged: {
        mySpinBox.setRealValue(mySpinBox.realValue) // sanitize and clamp realValue
        spinBoxInput.text = mySpinBox.textFromValue(mySpinBox.realValue, mySpinBox.locale)
        mySpinBox.value = 0 // Without setting value back to 0, it can happen that one of
                            // the indicator will be disabled due to range logic.
    }
    onRealValueModified: myTimer.restart()
    onFocusChanged: {
        if (mySpinBox.focus) {
            mySpinBox.dirty = false
        } else {
            // Make sure displayed value is correct after focus loss, as onEditingFinished
            // doesn't trigger when value is something validator doesn't accept.
            spinBoxInput.text = mySpinBox.textFromValue(mySpinBox.realValue, mySpinBox.locale)

            if (mySpinBox.dirty)
                spinBoxInput.handleEditingFinished()
        }
    }
    onDisplayTextChanged: spinBoxInput.text = mySpinBox.displayText
    onActiveFocusChanged: {
        if (mySpinBox.activeFocus) { // QTBUG-75862 && mySpinBox.focusReason === Qt.TabFocusReason)
            mySpinBox.preFocusText = spinBoxInput.text
            spinBoxInput.selectAll()
        }
    }

    Keys.onPressed: function(event) {
        if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
            event.accepted = true

            // Store current step size
            var currStepSize = mySpinBox.realStepSize

            // Set realStepSize according to used modifier key
            if (event.modifiers & Qt.ControlModifier)
                mySpinBox.realStepSize = mySpinBox.minStepSize

            if (event.modifiers & Qt.ShiftModifier)
                mySpinBox.realStepSize = mySpinBox.maxStepSize

            if (event.key === Qt.Key_Up)
                mySpinBox.realIncrease()
            else
                mySpinBox.realDecrease()

            // Reset realStepSize
            mySpinBox.realStepSize = currStepSize
        }

        if (event.key === Qt.Key_Escape) {
            spinBoxInput.text = mySpinBox.preFocusText
            mySpinBox.dirty = true
            spinBoxInput.handleEditingFinished()
        }
    }

    function clamp(v, lo, hi) {
        return (v < lo || v > hi) ? Math.min(Math.max(lo, v), hi) : v
    }

    function setValueFromInput() {
        if (!mySpinBox.dirty)
            return

        // FIX: This is a temporary fix for QTBUG-74239
        var currValue = mySpinBox.realValue

        // Call the function but don't use return value. The realValue property
        // will be implicitly set inside the function/procedure.
        mySpinBox.valueFromText(spinBoxInput.text, mySpinBox.locale)

        if (mySpinBox.realValue !== currValue) {
            mySpinBox.realValueModified()
        } else {
            // Check if input text differs in format from the current value
            var tmpInputValue = mySpinBox.textFromValue(mySpinBox.realValue, mySpinBox.locale)

            if (tmpInputValue !== spinBoxInput.text)
                spinBoxInput.text = tmpInputValue
        }

        mySpinBox.dirty = false
    }

    function setRealValue(value) {
        if (Number.isNaN(value))
            value = 0

        if (mySpinBox.decimals === 0)
            value = Math.round(value)

        mySpinBox.realValue = mySpinBox.clamp(value,
                                              mySpinBox.validator.bottom,
                                              mySpinBox.validator.top)
    }

    function realDecrease() {
        // Store the current value for comparison
        var currValue = mySpinBox.realValue
        mySpinBox.valueFromText(spinBoxInput.text, mySpinBox.locale)

        mySpinBox.setRealValue(mySpinBox.realValue - mySpinBox.realStepSize)

        if (mySpinBox.realValue !== currValue)
            mySpinBox.realValueModified()
    }

    function realIncrease() {
        // Store the current value for comparison
        var currValue = mySpinBox.realValue
        mySpinBox.valueFromText(spinBoxInput.text, mySpinBox.locale)

        mySpinBox.setRealValue(mySpinBox.realValue + mySpinBox.realStepSize)

        if (mySpinBox.realValue !== currValue)
            mySpinBox.realValueModified()
    }
}
